Repository navigation
Range#max: beginless Integer ranges, as 4.0 answers them - #892
Merged
Merged
Conversation
Five of the 27 examples that went red when rbgo moved to Ruby 4.0 (#799) are one cause: `core/range/max_spec.rb` has `ruby_version_is "4.0"` blocks that rbgo refused outright. The file goes 30 → 35, fail=0 error=0. ## The reference range.c's `range_max` at v4.0.9 changed in two places that both read as "a nil begin is not a disqualification": int c = NIL_P(b) ? -1 : OPTIMIZED_CMP(b, e); // beginless sorts BELOW the end if (!NIL_P(b) && !RB_INTEGER_TYPE_P(b)) { // the Integer-begin demand is rb_raise(... "cannot exclude end value with non Integer begin value"); } // now guarded by !NIL_P(b) and the n-argument branch goes through `reverse_each`, which walks `(NIL_P(beg) || RB_INTEGER_TYPE_P(beg)) && RB_INTEGER_TYPE_P(end)` downward from the end with nothing but n to stop it. ## What moved here * `(...1).max` is 0, not a TypeError. * `(..1).max(n)` and `(...1).max(n)` count down from the end, unbounded below. * Bignum endpoints answer arithmetically: `(0...2**64).max(2)` no longer tries to materialise 2**64 elements. The predecessor of an exclusive end is taken in big.Int, so the int64 floor stops being a special case — `(...-2**63).max` is -9223372036854775809, a Bignum, as in MRI. * A beginless NON-Integer range raises TypeError "can't iterate from NilClass", not the old blanket RangeError. * `(..1).max(0)` is `[]`. ## The guard order is observable `range_max` asks for a block BEFORE it looks at n. `(..1).max(2) { |a,b| a<=>b }` raises in MRI; answering the n first returns `[1, 0]` and looks entirely reasonable. The check moved to the top of the method for that reason, and the test says so. ## Two tests were MOVED, not relaxed `max_n_beginless` and `max_excl_beginless_int` in range_conformance_test.go pinned the pre-4.0 refusals — ruby/spec marks both `ruby_version_is ""..."4.0"`. Each row now names the refusal that replaced it, so the file still asserts a refusal for each shape rather than losing one. ## Measured, not read Every expectation came from MRI 4.0.7 on this host through rubydiff, before and after. Three probe programs covering 36 shapes: the first two now AGREE byte-for-byte; the third has one remaining divergence, pre-existing and unrelated to beginless ranges — `(1..10).max(2) { |a,b| -(a<=>b) }` is `[1, 2]` in MRI and `[10, 9]` here, because rbgo answers `max(n)` arithmetically and ignores the block's ordering. Filed separately rather than smuggled in. ## Bound A beginless range has no lower endpoint to clamp n against, so n alone decides the allocation: `(..1).max(2**40)` is a request for 8 TiB. MRI bounds it inside `rb_ary_new2`, which refuses a length above ARY_MAX_SIZE with ArgumentError; this refuses with the same class, and the test asserts both the refusal and that an ordinary count of 1000 still works. Coverage: integerRangeMaxN, rangeBigEnds and toBigInt at 100.0%. The one uncovered line before the last test was rangeBigEnds' non-Integer-begin refusal, reachable as `(1.0..3).max(2)`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Nov 4, 2026
tannevaled
added a commit
that referenced
this pull request
Nov 5, 2026
tannevaled
added a commit
that referenced
this pull request
Nov 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Five of the 27 examples that went red on the move to Ruby 4.0 (#799) share one
cause, and this closes it.
core/range/max_spec.rbgoes 30 → 35, fail=0error=0.
The reference
range.c'srange_maxatv4.0.9changed in two places that both say a nilbegin is not a disqualification:
and the n-argument branch runs through
reverse_each, which walks(NIL_P(beg) || RB_INTEGER_TYPE_P(beg)) && RB_INTEGER_TYPE_P(end)downwardfrom the end with nothing but
nto stop it.What moved
(...1).max0(..1).max(2)[1, 0](...1).max(2)[0, -1](0...2**64).max(2)can't iterate from 0[18446744073709551615, …614](..1.0).max(2)can't iterate from NilClass(..1).max(0)[](...-2**63).max-9223372036854775809(Bignum)The predecessor of an exclusive end is taken in
big.Intnow, so the int64floor stops being a special case — and
(0...2**64).max(2)no longer tries tomaterialise 2⁶⁴ elements.
⛔ The guard order is observable
range_maxasks for a block before it looks atn.(..1).max(2) { |a,b| a<=>b }raises in MRI; answering the
nfirst returns[1, 0]and looks entirelyreasonable. The check moved to the top of the method for that reason, and a
test names it.
⛔ Two tests were MOVED, not relaxed
max_n_beginlessandmax_excl_beginless_intpinned the pre-4.0 refusals —ruby/spec marks both
ruby_version_is ""..."4.0". Each row now names therefusal that replaced it, so the file still asserts a refusal for each shape
instead of losing one.
Measured, not read
Every expectation came from MRI 4.0.7 on this host through
rubydiff, beforeand after. Two of the three probe programs now AGREE byte-for-byte; the
third keeps one divergence that is pre-existing and has nothing to do with
beginless ranges —
(1..10).max(2) { |a,b| -(a<=>b) }is[1, 2]in MRI and[10, 9]here. Filed as #891 rather than smuggled in; the single upstreamspec for that shape uses the identity comparison, so it cannot tell the two
apart.
specdelta, provenanced, over all 34core/rangefiles:Bound
A beginless range has no lower endpoint to clamp
nagainst, sonalonedecides the allocation:
(..1).max(2**40)is a request for 8 TiB from one lineof untrusted Ruby. MRI bounds it inside
rb_ary_new2, which refuses a lengthabove
ARY_MAX_SIZEwith ArgumentError; this refuses with the same class. Thetest asserts the refusal and that an ordinary count of 1000 still works — a
cap that refused everything would pass the first half alone.
Coverage:
integerRangeMaxN,rangeBigEnds,toBigIntat 100.0%. The lastuncovered line was
rangeBigEnds' non-Integer-begin refusal, reachable as(1.0..3).max(2); the coverage gate found it, not me.🤖 Generated with Claude Code